﻿using System.Collections.Generic;
using System.Data.Common;
using System.Data.Linq;
using System.IO;
using System.Text;
using Antlr.Runtime;
using org.mayb.Fixtures.Parser;
using System.Reflection;
using System;

namespace org.mayb.Fixtures
{
    public class Fixtures
    {
        private static Dictionary<string, object> processed = new Dictionary<string, object>();
        private static List<ObjectInfo> objects = new List<ObjectInfo>();

        private static List<string> files = new List<string>();
        private static List<string> usedIDs = new List<string>();

        private static readonly List<Assembly> assemblies = new List<Assembly>();
        /// <summary>
        /// 
        /// </summary>
        /// <param name="assembly"></param>
        public static void RegisterAssembly(Assembly assembly)
        {
            assemblies.Add(assembly);
        }

        internal static Type FindType(string typeName)
        {
            foreach (Assembly assembly in assemblies)
            {
                foreach (Type t in assembly.GetTypes())
                {
                    if (t.IsEnum)
                        continue;
                    if (t.Name == typeName || t.FullName == typeName)
                        return t;
                }
            }
            return null;
        }

        internal static object CreateInstance(Type t)
        {
            ConstructorInfo ci = t.GetConstructor(new Type[] { });
            if (ci == null)
                throw new FixtureException(string.Format("No parameterless constructor for type {0}.", t.Name));
            return ci.Invoke(null);
        }

        private static void PopulateFileList(string directory)
        {
            foreach (string file in Directory.GetFiles(directory, "*.fix", SearchOption.AllDirectories))
            {
                files.Add(file);
            }
        }

        private static void ParseFiles()
        {
            foreach (string file in files)
            {
                ANTLRFileStream afs = new ANTLRFileStream(file);
                FixtureLexer fl = new FixtureLexer(afs);
                CommonTokenStream cts = new CommonTokenStream(fl);
                FixtureParser fp = new FixtureParser(cts, objects, usedIDs);

                fp.top();
            }
        }

        private static bool ShouldDefer(ObjectInfo oi)
        {
            foreach (string refer in oi.References)
            {
                if (!processed.ContainsKey(refer))
                    return true;
            }

            return false;
        }

        internal static object GetTypedValue(Type propertyType, string value)
        {
            if (propertyType == typeof(string))
                return value;

            if (propertyType.IsEnum)
                return Enum.Parse(propertyType, value, true);
            if (propertyType.IsGenericType && propertyType.Name.StartsWith("Nullable"))
            {
                Type basePropertyType = propertyType.GetGenericArguments()[0];
                return GetTypedValue(basePropertyType, value);
            }

            MethodInfo mi = propertyType.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null);
            if (mi == null)
                throw new FixtureException(string.Format("Unabled to convert {0} to type {1}.", value, propertyType.Name));

            return mi.Invoke(null, new object[] { value });
        }

        internal static void SetProperty(object obj, object value, PropertyInfo pi)
        {
            if (pi == null || !pi.CanWrite)
                throw new Exception(string.Format("Type {0} has no public set-able property {1}.", obj.GetType().Name, pi.Name));

            pi.SetValue(obj, value, null);
        }

        private static void ProcessObjects()
        {
            List<ObjectInfo> deferred = objects;
            int oldDeferredCount;

            do
            {
                oldDeferredCount = deferred.Count;
                List<ObjectInfo> toProc = deferred;
                deferred = new List<ObjectInfo>();

                foreach (ObjectInfo oi in toProc)
                {
                    if (ShouldDefer(oi))
                    {
                        deferred.Add(oi);
                        continue;
                    }

                    Type type = FindType(oi.Type);
                    if (type == null)
                        throw new FixtureException(string.Format("Type {0} not defined in registered assemblies. File: {1} - {2}:{3}", oi.Type, oi.File, oi.Line, oi.Char));

                    object instance = oi.BuildObject();

                    if(processed.ContainsKey(oi.ID))
                    {
                        throw new ApplicationException(string.Format("Fixtures contain a duplicate identifier: {0} File: {1} - {2}:{3}", oi.ID, oi.File, oi.Line, oi.Char));
                    }

                    processed.Add(oi.ID, instance);
                }

            } while (oldDeferredCount > deferred.Count);

            if (deferred.Count > 0)
            {
                string derrors = BuildDeferredErrors(deferred);
                throw new FixtureException("Unable to load all fixtures. There is either a circular reference or an undefined reference.\n" + derrors);
            }
        }

        private static void TestProcessObjects()
        {
            List<ObjectInfo> deferred = objects;
            int oldDeferredCount;

            do
            {
                oldDeferredCount = deferred.Count;
                List<ObjectInfo> toProc = deferred;
                deferred = new List<ObjectInfo>();

                foreach (ObjectInfo oi in toProc)
                {
                    if (ShouldDefer(oi))
                    {
                        deferred.Add(oi);
                        continue;
                    }

                    Type type = FindType(oi.Type);
                    if (type == null)
                        Console.Error.WriteLine(string.Format("Type {0} not defined in registered assemblies. File: {1} - {2}:{3}", oi.Type, oi.File, oi.Line, oi.Char));
                    else
                    {
                        try
                        {
                            object instance = oi.BuildObject();

                            processed.Add(oi.ID, instance);
                        }
                        catch (Exception ex)
                        {
                            System.Console.Error.WriteLine(ex.Message);
                        }
                    }
                }

            } while (oldDeferredCount > deferred.Count);

            if (deferred.Count > 0)
            {
                string derrors = BuildDeferredErrors(deferred);
                Console.Error.WriteLine("Unable to load all fixtures. There is either a circular reference or an undefined reference.\n" + derrors);
            }
        }

        private static string BuildDeferredErrors(IEnumerable<ObjectInfo> deferred)
        {
            StringBuilder sb = new StringBuilder();

            foreach (ObjectInfo oi in deferred)
            {
                sb.AppendFormat("{0} ({1}@{2}:{3} References: ", oi.ID, oi.File, oi.Line, oi.Char);

                foreach (string or in oi.References)
                {
                    sb.Append(or + ", ");
                }

                sb.Remove(sb.Length - 2, 2);

                sb.Append("\n");
            }

            return sb.ToString();
        }

        private static void StoreInDataContext(DataContext dc)
        {
            foreach (object obj in processed.Values)
            {
                ITable table = dc.GetTable(obj.GetType());
                if (table != null)
                {
                    table.InsertOnSubmit(obj);
                }
            }

            dc.SubmitChanges();
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="directories"></param>
        /// <param name="dc"></param>
        public static void LoadFixtures(string[] directories, DataContext dc)
        {
            processed = new Dictionary<string, object>();
            objects = new List<ObjectInfo>();
            files = new List<string>();
            usedIDs = new List<string>();

            foreach (string dir in directories)
            {
                PopulateFileList(dir);
            }

            ParseFiles();
            ProcessObjects();



            StoreInDataContext(dc);
        }

        public static void TestFixtures(string[] directories)
        {
            TestFixtures(directories, null);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="directories"></param>
        /// <param name="dc"></param>
        public static void TestFixtures(string[] directories, DataContext dc)
        {
            processed = new Dictionary<string, object>();
            objects = new List<ObjectInfo>();
            files = new List<string>();
            usedIDs = new List<string>();

            Console.WriteLine("Syntax Errors:");
            foreach (string dir in directories)
            {
                PopulateFileList(dir);
            }
            try
            {
                ParseFiles();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Unhandeled Exception: " + ex.Message);
            }
            Console.WriteLine("Fixture Issues:");
            TestProcessObjects();

            if (dc != null)
            {
                Console.WriteLine("SQL Issues:");
                if (dc.Connection.State == System.Data.ConnectionState.Closed)
                    dc.Connection.Open();
                DbTransaction trans = dc.Connection.BeginTransaction();

                dc.Transaction = trans;

                try
                {
                    StoreInDataContext(dc);
                }
                catch (Exception ex)
                {
                    System.Console.Error.WriteLine(ex.Message);
                    System.Console.Error.WriteLine(ex.StackTrace);
                }

                trans.Rollback();
            }
        }
        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="id"></param>
        /// <returns></returns>
        public static T GetFixture<T>(string id)
        {
            if (!processed.ContainsKey(id))
                throw new FixtureException(string.Format("Fixture with id = {0} not found.", id));

            object obj = processed[id];
            if (obj is T)
                return (T)obj;

            throw new FixtureException(string.Format("Fixture {0} is not of correct type.", id));
        }

        internal static object GetFixture(string id)
        {
            if (!processed.ContainsKey(id))
                throw new FixtureException(string.Format("Fixture with id = {0} not found.", id));
            return processed[id];
        }
    }
}
